package intervalMath;
import java.math.*;
/**
 *
 * @author Vera Morozova
 */
public class ElementaryMath {
    static int ITERATIONS_COUNT = 15;
    static int EXP_ITERATIONS_COUNT = 15;
    static BigDecimal PI = new BigDecimal("3.14159265358979323846264338327950288419716939937510582097494459230781640628620"
            + "899862803482534211706798214808651328230664709384460955058223172535940812848111"
            + "745028410270193852110555964462294895493038196442881097566593344612847564823378"
            + "678316527120190914564856692346034861045432664821339360726024914127372458700660"
            + "631558817488152092096282925409171536436789259036001133053054882046652138414695"
            + "194151160943305727036575959195309218611738193261179310511854807446237996274956"
            + "735188575272489122793818301194912983367336244065664308602139494639522473719070");
    private static BigDecimal half = new BigDecimal(0.5);

    public static BigDecimal sqrt(BigDecimal a, int scale, RoundingMode rm) throws IntervalException{
        if (a.compareTo(BigDecimal.ZERO)<=0)throw new IntervalException("Argument is negative");
        int iter = 0;
        if (a.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
        BigDecimal result = sqrtIteration(a,iter,a,scale, rm);
        result = result.setScale(scale, RoundingMode.FLOOR);
        return result;
    }

    private static BigDecimal sqrtIteration(BigDecimal a, int iter, BigDecimal x, int scale, RoundingMode rm){
        iter++;
        BigDecimal Xn = (x.add(a.divide(x,scale, rm))).multiply(half);
        double eps = Math.pow(10, -1*scale);
        if (x.subtract(Xn).abs().compareTo(new BigDecimal(eps))<=0|| (Xn.compareTo(BigDecimal.ZERO) == 0))
            return Xn;
        else return sqrtIteration(a, iter, Xn, scale, rm);
    }

    private static BigDecimal[] expArgConversion(BigDecimal x){
        BigDecimal log2 = new BigDecimal(Math.log(2));
        BigDecimal[] divResult = x.divideAndRemainder(log2);
        return divResult;
    }

    public static BigDecimal expTaylorSeries(BigDecimal x,  int scale, RoundingMode rm){
       int iter = 0;
       BigDecimal xPrev = BigDecimal.ONE;
       BigDecimal sum = BigDecimal.ONE;
       double eps = Math.pow(10, -1*scale);
       do{
            iter++;
            BigDecimal xN = x.divide(new BigDecimal(iter),scale, rm).multiply(xPrev);
            if(xPrev.subtract(xN).abs().compareTo(new BigDecimal(eps))<=0)
                break;
            xPrev = xN;
            sum = sum.add(xPrev);
        }while(true);
       return sum;
    }
    public static BigDecimal expBigDecimal(BigDecimal x,  int scale, RoundingMode rm){
       int t = 2;
       x = x.movePointLeft(t);      
       BigDecimal sum = expTaylorSeries(x, scale, rm);
       return sum.pow((int)Math.pow(10, t));
    }

    public static BigDecimal exp(BigDecimal x,  int scale, RoundingMode rm){
       BigDecimal[] arg = expArgConversion(x);
       x = arg[1];
       BigDecimal sum = expTaylorSeries(x, scale, rm);
       BigDecimal result = sum.multiply(new BigDecimal(Math.pow(2,arg[0].doubleValue())));
       result = result.setScale(scale, RoundingMode.FLOOR);
       return result;
    }

    public static BigDecimal sinArgConversion(BigDecimal x,  int scale, RoundingMode rm){
        BigDecimal[] div = x.divideAndRemainder(PI);
        BigDecimal two  = new BigDecimal(2);
        BigDecimal negativeTwo  = new BigDecimal(-2);
        BigDecimal r = div[1];
        BigDecimal y = BigDecimal.ZERO;
        BigDecimal n = BigDecimal.ZERO;
        if (r.compareTo(PI.divide(two, scale, rm))<0 && r.compareTo(PI.divide(negativeTwo, scale, rm))>0){
            y = r;
            n = div[0];
        }else{
            if (r.compareTo(BigDecimal.ZERO)>0){
                y = r.subtract(PI);
                n = div[0].subtract(BigDecimal.ONE);
            }else{
                y = r.add(PI);
                n = div[0].add(BigDecimal.ONE);
            }
        }
        BigDecimal arg = y.multiply(new BigDecimal(2)).divide(PI,scale, rm);
        arg = arg.multiply(new BigDecimal(-1).pow(Math.abs(n.intValue())));
        return arg;
    }

    public static BigDecimal polynomTchebyshev(int n, BigDecimal x){
        if (n==0) return BigDecimal.ONE;
        if (n==1) return x;
        BigDecimal Tn = polynomTchebyshev(n-1,x);
        BigDecimal Tnn = polynomTchebyshev(n-2,x);
        BigDecimal res = x.multiply(new BigDecimal(2)).multiply(Tn).subtract(Tnn);
        return res;
    }
    public static double coeffTchebyshev(int j, BigDecimal x, int N){
        double sum = 0;
        for (int k=0;k<N;k++){
            double a = Math.cos(Math.PI*(k+0.5)/(double)N);
            a = Math.sin(a*Math.PI/2.0);
            double b = Math.PI*j*(k+0.5)/(double)N;
            b = Math.cos(b);
            sum += a*b;
        }
        return  sum*2/(double)N;
    }
    public static BigDecimal sin(BigDecimal x,  int scale, RoundingMode rm){
        x = sinArgConversion(x,scale, rm);
        int N = 10;
        BigDecimal sum = BigDecimal.ZERO;
        BigDecimal c0 = new BigDecimal(0.5*coeffTchebyshev(0, x, N));
        for (int j = 0; j < N; j++){
            BigDecimal c = new BigDecimal(coeffTchebyshev(j, x, N));
            BigDecimal p = polynomTchebyshev(j,x);
            sum = sum.add(c.multiply(p));
        }
        BigDecimal result = sum.subtract(c0);
        result = result.setScale(scale, RoundingMode.FLOOR);
        return result;
    }
    public static BigDecimal cos(BigDecimal x,  int scale, RoundingMode rm){
        BigDecimal two  = new BigDecimal(2);
        BigDecimal doubleSin = sin(x.divide(two),scale,rm).pow(2);
        BigDecimal cos = BigDecimal.ONE.subtract(two.multiply(doubleSin));
        return cos;
    }

    private static BigDecimal[] lnArgConversion(BigDecimal x,  int scale, RoundingMode rm){
        BigDecimal[] result = new BigDecimal[2];
        int p = 0;
        BigDecimal y = BigDecimal.ZERO;
        BigDecimal left = new BigDecimal(1./Math.sqrt(2));
        BigDecimal right = new BigDecimal(Math.sqrt(2));
        if (x.compareTo(left)<0){
            do{
                p++;
                y = x.multiply(new BigDecimal(2).pow(p));
            }while(!(y.compareTo(left)>=0 && y.compareTo(right)<=0));
            result[0] = y;
            result[1] = new BigDecimal(-p);
        }
        if(x.compareTo(right)>0){
            do{
                p++;
                y = x.divide(new BigDecimal(2).pow(p),scale, rm);
            }while(!(y.compareTo(left)>=0 && y.compareTo(right)<=0));
            result[0] = y;
            result[1] = new BigDecimal(p);
        }
        if(x.compareTo(left)>=0 && x.compareTo(right)<=0){
            result[0] = x;
            result[1] = BigDecimal.ZERO;
        }
        return result;
    }

    public static BigDecimal taylorSeries(BigDecimal arg,  int scale, RoundingMode rm){
        int k = 1;
        BigDecimal sum = BigDecimal.ZERO;
        double eps = Math.pow(10, -1*scale);
        do{
            double coeff = 1./(2*k-1);
            BigDecimal nextSum = arg.subtract(BigDecimal.ONE).divide(arg.add(BigDecimal.ONE),scale,rm).pow(2*k-1);
            if (nextSum.abs().compareTo(new BigDecimal(eps))<=0)
                break;
            sum = sum.add(nextSum.multiply(new BigDecimal(coeff)));
            k++;
        }
        while(true);
        return sum;
    }

     public static BigDecimal ln(BigDecimal x, int scale, RoundingMode rm) throws IntervalException{
         if (x.compareTo(BigDecimal.ZERO)<=0)throw new IntervalException("Argument is negative");
         BigDecimal[] arg = lnArgConversion(x,scale,rm);
         BigDecimal sum = taylorSeries(arg[0],scale,rm).multiply(new BigDecimal(2));
         BigDecimal log2 = new BigDecimal(Math.log(2));
         BigDecimal result = arg[1].multiply(log2).add(sum);
         result = result.setScale(scale, RoundingMode.FLOOR);
         return result;
     }

}
